/**
 * Copyright (c) 2008, Fabien Antoine
 * All rights reserved.
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *   - Redistributions of source code must retain the above copyright
 *     notice, this list of conditions and the following disclaimer.
 *   - Redistributions in binary form must reproduce the above copyright
 *     notice, this list of conditions and the following disclaimer in the
 *     documentation and/or other materials provided with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE HAXE PROJECT CONTRIBUTORS "AS IS" AND ANY
 * EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE HAXE PROJECT CONTRIBUTORS BE LIABLE FOR
 * ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
 * OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH
 * DAMAGE.
 */

package fas.massload;

import fas.massload.file.ILoadFile;
import fas.massload.file.ULoadFile;
import fas.massload.file.LLoadFile;
import fas.massload.file.SLoadFile;
import fas.massload.file.GLoadFile;
import fas.massload.events.MassLoaderEvent;
import fas.massload.events.MassLoaderEvent;

import flash.events.EventDispatcher;


class MassLoader extends EventDispatcher
{
	// --------- //
	// Variables //
	// --------- //
	
	private static var _instance:MassLoader = null;
	private static var _canInstanciate:Bool	= false;
	
	
	
	// ----------- //
	// Constructor //
	// ----------- //
	
	/**
	 * Private constructor of MassLoader.
	 * Can't be instanciate by this way: you need to use getInstance() method
	 */
	function new()
	{
		super();
		
		// If the allow instance is set from the getInstance method, we can create the instance
		// Else, we throw an error
		if( _canInstanciate )
		{
			// Initialise variables
			files = new Array<ILoadFile>();
			loading = false;
			
			parallelFiles = 1;
			loadingFiles = 0;
			
			// Disable creation of a new instance
			_canInstanciate = false;
		}else throw "MassLoader is a singleton class! Impossible to instanciate it with its constructor!";
	}
	
	
	
	// -------------- //
	// Public methods //
	// -------------- //
	
	/**
	 * If no instance of Massloader exist we create one, and in all cases we return the instance
	 * @param	autoStart		Bool			True if you to start the loader, false else
	 * @return					MassLoader		The instance of MassLoader
	 */
	public static function getInstance( ?autoStart:Bool=false ):MassLoader
	{
		// If the class isn't instantied, we create this one
		if( _instance == null )
		{
			// We set the variable to allow new instance
			_canInstanciate = true;
			
			// We create the instance
			_instance = new MassLoader();
		}
		
		// If auto start is selected, we start the loader
		if( autoStart ) _instance.start();
		
		// And we return the instance
		return _instance;
	}
	
	
	/**
	 * Launch the downloading of files in the list
	 */
	public function start():Void
	{
		if( !loading )
		{
			// Set the state to "loading"
			loading = true;
			
			// We start maximum files we can or are allowed
			var i:Int = 0;
			while( i < parallelFiles && i < files.length )
				launchFile( files[ i++ ] );
		}
	}
	
	
	/**
	 * Stop the downloading of files in the list
	 */
	public function stop():Void
	{
		if( loading )
		{
			// Set the state to "not loading"
			loading = false;
			
			// Stop all files on loading
			for( i in 0...loadingFiles )
				stopFile( files[ i ] );
		}
	}
	
	
	/**
	 * Return a concatenation of informations about this instance of MassLoader
	 * @return					String			Concatenation of MassLoader informations
	 */
	public override function toString():String
	{
		var sTexte:String = "";
		for( i in 0...files.length )
		{
			sTexte += "\n" + i + "->" + files[ i ].toString();
		}
		
		return "Files names: " + sTexte;
	}
	
	
	/**
	 * Adds a file to the list of downloads
	 * @param		file			ILoadFile		LoadFile we must download
	 * @param		bPriority		Priority		Priority of add
	 * @param		bDeleteHomonym	Bool			Indicate file homonyms are deleted
	 */
	public function addFile( file:ILoadFile, ?uiPriority:Priority, ?bDeleteHomonym:Bool=false ):Void
	{
		if( uiPriority == null ) uiPriority = Priority.Low;
		
		// If the file is null we create an Exception
		if( file == null ) throw "Unexpected null value for LoadFile";
		
		// If the name is already present and we have to delete homonyms, we delete all its homonyms
		if( bDeleteHomonym && exists( file.name ) ) removeFile( file.name );
		
		
		// Switch the priority and works depending this one
		switch( uiPriority )
		{
			// If loading, stop the current load et add file in first
			// Else, add in first position
			case High:
				if( loading )
				{
					// Stop the last started download, if no places are available
					if( files.length > 0 && loadingFiles == parallelFiles )
					{
						loadingFiles--;
						files[ loadingFiles ].close();
						unRegisterEvents( files[ loadingFiles ] );
					}
					
					// Adds on first position
					files.unshift( file );
					
					// Launchs loading
					launchFile( files[0] );
				}else
				{
					// Adds on first position
					files.unshift( file );
				}
			
			// If loading, adds file after the file on load
			// Else, add in first position
			case Medium:
				if( loading )
				{
					// If there are no files on loading, we push this one on loading
					// Else, we only added it
					if( loadingFiles < parallelFiles && files.length < parallelFiles )
					{
						files.unshift( file );
						launchFile( files[0] );
					}else
					{
						// Add after loading files
						files.insert( Std.int( Math.min( files.length, loadingFiles ) - 1 ), file );
					}
				}else
				{
					// Add after files we will be loaded
					files.insert( cast( Math.min( files.length, loadingFiles ) - 1, Int ), file );
				}
			
			// Add file at the end of the list
			case Low:
				files.push( file );
				
				// If we are downloading and can start the added file, we start it
				if( loading && loadingFiles < parallelFiles && loadingFiles < files.length )
					launchFile( files[ files.length - 1] );
		}
	}
	
	/**
	 * Delete a file from the list of downloads
	 * @param		sName		String			LoadFiles'name we must delete from the liste of downloads
	 */
	public function removeFile( sName:String ):Void
	{
		// If the LoadFile is in the list we remove it
		if( exists( sName ) )
		{
			var newFiles:Array<ILoadFile> = new Array<ILoadFile>();
			
			// Filter the list to close and destroy the selected LoadFile
			for( i in files )
			{
				if( i.name == sName )
				{
					if( i.loading ) i.close();
					newFiles.push( i );
				}
			}
			
			// Save the new array
			files = newFiles;
			
			// If the removed file is the last, we dispatch a COMPLETE_ALL event
			if( files.length == 0 ) dispatchEvent( new MassLoaderEvent( MassLoaderEvent.COMPLETE_ALL ) );
		}
	}
	
	
	/**
	 * Replace a file from the list of downloads by a new one
	 * @param		file		ILoadFile		LoadFile we must add in the liste of downloads
	 * @param		sName		String			Name of the LoadFile to replace
	 * @param		bAll		Bool			True if you want to replace all homonyms, false if you want to replace the first
	 */
	public function replaceFile( file:ILoadFile, sName:String, bAll:Bool ):Void
	{
		var index:Int;
		
		// If the LoadFile is in the list we replace it
		if( exists( file.name ) )
		{
			// If not "bAll", we replace only the first file
			// Else replace all homonyms
			if( !bAll )
			{
				// Gets the index of the file
				index = indexOf( sName );
			
				// Replaces the selected file
				files.splice( index, 1 );
				files.insert( index, file );
			}else
			{
				while( ( index = indexOf( sName ) ) != -1 )
				{
					// Replaces the selected file
					files.splice( index, 1 );
					files.insert( index, file );
				}
			}
		}
	}
	
	
	/**
	 * Return the presence of a LoadFile on the list
	 * @param		sName		String			LoadFile's name we verify the existence
	 * @return					Bool			True if the LoadFile is present, False else
	 */
	public function exists( sName:String ):Bool
	{
		for( i in files )
			if( i.name == sName ) return true;
		return false;
	}
	
	
	/**
	 * Return the first index of a LoadFile in the list
	 * @param		sName		String			LoadFile's name we verify the existence
	 * @return				Int				The value of the index (or -1 if the item doesn't exists)
	 */
	public function indexOf( sName:String ):Int
	{
		// If the array doesn't have items, we return -1
		if( !exists( sName ) ) return -1;
		
		var i:Int = 0;
		var index:Int = -1;
		
		// Verify all the items on the array
		while( i < files.length && index == -1 )
		{
			if( files[ i ].name == sName ) index = i;
			else i++;
		}
		
		return index;
	}
	
	/**
	 * Return the last index of a LoadFile in the list
	 * @param		sName		String			LoadFile's name we verify the existence
	 * @return				Int				The value of the index (or -1 if the item doesn't exists)
	 */
	public function lastIndexOf( sName:String ):Int
	{
		// If the array doesn't have items, we return -1
		if( !exists( sName ) ) return -1;
		
		var i:Int = files.length - 1;
		var index:Int = -1;
		
		// Verify all the items on the array
		while( i >= 0 && index == -1 )
		{
			if( files[ i ].name == sName ) index = i;
			else i++;
		}
		
		return index;
	}
	
	// --------------- //
	// Private methods //
	// --------------- //
	
	/**
	 * Add all events listeners for the Loader
	 */
	private function registerEvents( file:ILoadFile ):Void
	{
		// Adds events listener
		file.addEventListener( MassLoaderEvent.COMPLETE, onComplete );
		file.addEventListener( MassLoaderEvent.CLOSE, onClose );
		file.addEventListener( MassLoaderEvent.PROGRESS, onProgress );
	}
	
	/**
	 * Remove all events listeners for the Loader
	 */
	private function unRegisterEvents( file:ILoadFile ):Void
	{
		// Removes events listener
		file.removeEventListener( MassLoaderEvent.COMPLETE, onComplete );
		file.removeEventListener( MassLoaderEvent.CLOSE, onClose );
		file.removeEventListener( MassLoaderEvent.PROGRESS, onProgress );
	}
	
	/**
	 * Register events and launch the download of a file
	 * @param		file			ILoadFile			File to start download
	 */
	private function launchFile( file:ILoadFile ):Void
	{
		// If the massloader is running, we can launch the file
		if( loading )
		{
			registerEvents( file );
			file.load();
			
			// Increase number of downloads
			loadingFiles++;
		}
	}
	
	/**
	 * Unegister events and stop the download of a file
	 * @param		file			ILoadFile			File to start download
	 */
	private function stopFile( file:ILoadFile ):Void
	{
		unRegisterEvents( file );
		file.close();
		
		// Decrease number of downloads
		loadingFiles--;
	}
	
	
	// ------ //
	// Events //
	// ------ //
	
	/**
	 * Event called when close loading
	 * @param	ev			MassLoaderEvent		Event received
	 */
	private function onClose( ev:MassLoaderEvent ):Void
	{
		// Decrease number of loading files
		loadingFiles--;
		
		// Remove the closed file
		//_files.splice( 0, 1 );
		files.splice( indexOf( ev.file.name ), 1 );
		
		// Dispatch the close event
		dispatchEvent( new MassLoaderEvent( MassLoaderEvent.CLOSE, ev.file ) );
		
		// If the massloader is running, we start the next file
		if( loading )
		{
			// If there are no files in the list, we dispatch the all complete event
			// Else if a place is available in the list, launch the next file
			if( files.length == 0 ) dispatchEvent( new MassLoaderEvent( MassLoaderEvent.COMPLETE_ALL ) );
			else if( loadingFiles < parallelFiles && loadingFiles < files.length )
			{
				launchFile( files[ loadingFiles ] );
			}
		}
	}
	
	/**
	 * Event called when complete loading
	 * @param	ev			MassLoaderEvent		Event received
	 */
	private function onComplete( ev:MassLoaderEvent ):Void
	{
		// Decrease number of loading files
		loadingFiles--;
		
		// Remove the loaded file
		files.splice( indexOf( ev.file.name ), 1 );
		
		// Dispatch the complete event
		dispatchEvent( new MassLoaderEvent( MassLoaderEvent.COMPLETE_ELEM, ev.file ) );
		
		// If the massloader is running, we start the next file
		if( loading )
		{
			// If there are no files in the list, we dispatch the all complete event
			// Else if a place is available in the list, launch the next file
			if( files.length == 0 ) dispatchEvent( new MassLoaderEvent( MassLoaderEvent.COMPLETE_ALL ) );
			else if( loadingFiles < parallelFiles && loadingFiles < files.length ) launchFile( files[ loadingFiles ] );
		}
	}
	
	/**
	 * Event called when progress loading
	 * @param	ev			MassLoaderEvent		Event received
	 */
	private function onProgress( ev:MassLoaderEvent ):Void
	{
		// Dispatch the event
		dispatchEvent( new MassLoaderEvent( MassLoaderEvent.PROGRESS, ev.file, ev.bytesLoaded, ev.bytesTotal, ev.element ) );
	}
	
	
	// --------- //
	// Accessors //
	// --------- //
	
	/**
	 * State of loading
	 */
	public var loading( default, null ):Bool;
	
	/**
	 * Array containing all LoadFiles
	 */
	public var files( default, null ):Array<ILoadFile>;
	
	/**
	 * Count of all LoadFiles
	 */
	public var filesCount( getFilesCount, null ):Int;
	private function getFilesCount():Int { return files.length; }
	
	/**
	 * Number of LoadFiles currently on loading
	 */
	public var loadingFiles( default, null ):Int;
	
	/**
	 * Number of parallel downloads available
	 */
	public var parallelFiles( getParallelFiles, setParallelFiles ):Int;
	private function getParallelFiles():Int { return parallelFiles; }
	private function setParallelFiles( v:Int ):Int { return parallelFiles = Std.int( Math.max( 1, v ) ); }
}

enum Priority
{
	High;
	Medium;
	Low;
}